feat(mobile): glassmorphic root headers with auto-hiding chrome - #14575
Conversation
Give the root tab screens a frosted header that content scrolls behind, matching the desktop client's `Frosted` surface, and let the whole top and bottom chrome get out of the way as you scroll. In scope: Feed, Trending, Notifications. Explore keeps its own search header and Library is deliberately untouched (its material top-tab bar is opaque and shared with Profile, so the glass had a visible seam there). How it fits together: - `GlassSurface` is the shared frosted surface. Real `BlurView` on iOS; on Android a near-opaque tint instead, matching the existing `ProfileNavOverlay` precedent — Android blur on a surface composited every scroll frame is a real perf risk. - `GlassChromeContext` owns one `scrollY` shared value. Everything reacts to it: the separator fades in over the first 24px, and the header and tab bar translate away on scroll-down and return on scroll-up. One signal keeps them in sync rather than three that can drift. - Headers float (`headerTransparent`), so screens pad their own scroll content by the measured stack height. Measured, not computed, because the safe-area inset varies per device and `OtaUpdateBanner` adds a row conditionally. - The bottom tab bar is now positioned absolutely. In normal flow the navigator reserved its height, so hiding it only uncovered dead background; now content occupies that space and slides behind the glass. `PlayBarChin` becomes `BottomChin` and now covers both bars. The name mattered: it reserved only the play bar's height, and with the tab bar out of flow the end of every list would otherwise sit behind it permanently — worst on short lists, which never scroll far enough for the auto-hide to uncover them. This is about the end of the content being reachable, not about avoiding overlap mid-scroll; content still passes under the glass while scrolling. No changeset on purpose: this has no SDK surface, and a `'@audius/mobile'` bump moves the OTA release-history routing key (see #14564). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Dark mode pass — ✅ passes, no code changes neededVerified on iOS simulator by switching system appearance to dark (theme mode follows system).
Also checked the theme matrix rather than just light/dark: One optional refinement, not blocking: the bottom separator ( Still unverified: Android. The |
Web Tests failed on responsiveCoverage.test.ts: the Discover Weekly page uses TrackTableLineup but had no entry in RESPONSIVE_TABLE_POLICIES, and that test governs the invariant that every shared-table consumer declares one. Adds a discoverWeeklyTracks policy, registers the page in the audited consumer list, and actually passes the policy to the table -- without that last part the entry would have satisfied the test while the table stayed non-responsive. Merge conflict in AppTabScreen.tsx was purely indentation: #14575 re-indented the Stack.Screen block. Took main's formatting and re-added the DiscoverWeeklyScreen registration at the matching depth. Full web suite passes locally (152 passed, 9 skipped). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to #14575, from the Android pass. **This is a real defect in what we just merged.** ## The bug Android has no backdrop blur, so `GlassSurface` fell back to a `0.94` tint to keep a hint of translucency. That only works in light mode. Translucency reads as *frost* when something blurs what shows through. With no blur, whatever bleeds through keeps its edges and reads as legible ghost text sitting on top of the header. And the same alpha lands completely differently depending on the surface: | | glass | ghost peak | relative contrast | |---|---|---|---| | Light | RGB 248 | 229 | ~8% | | Dark | RGB 14 | 27 | **~93%** | 6% of near-white content over a near-black surface nearly doubles the luminance. In dark mode, track titles, artists, durations and play counts were plainly readable behind the header. ## The fix Android is now fully opaque — which is what the `ProfileNavOverlay` fallback this was meant to follow already does (`backgroundColor: backgroundSurface`, no opacity). I cited that precedent in #14575 and then didn't actually follow it; that was the bug. No single alpha suits both themes, so Android trades the hint of translucency for a clean surface. Content still slides under the header, it just isn't see-through. **iOS is unchanged** and keeps its real blur. ## Verification Built `assembleProdDebug` and ran on a Pixel 8 Pro (API 35) emulator in dark mode with real content scrolled behind the header — the surface is now clean, and content clips at the glass edge instead of bleeding through. `tsc` 0 errors, `eslint` clean. Also confirmed on Android during the same pass, unchanged by this fix: tab bar position/elevation and z-order against the play bar, now-playing drawer and left nav drawer; auto-hide of both bars; and content offset (at max scroll the last item lands exactly at the play bar top). ## Known, not addressed here The play bar doesn't participate in auto-hide, so hiding the chrome mid-playback leaves it floating with an empty band beneath it. Not Android-specific and pre-existing from #14575 — worth a separate change if we want the bottom chrome to move as one unit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…14577) Follow-up to #14575. The now-playing bar didn't participate in the auto-hide, so scrolling down with a track playing left it stranded — the tab bar floats away beneath it and the play bar stayed put with an empty band underneath. It now travels on the same `useTabBarHiddenProgress` the tab bar uses, so the two move as one unit rather than on two signals that can drift. ## Why the whole drawer moves, not the play bar My first attempt translated just the play bar. Both bars hid — and it left an **empty white band** instead. The white surface behind the play bar belongs to the drawer root, so moving only its contents slid the bar off a background that stayed put. Same problem, different colour. So this moves the drawer. Two consequences worth reviewing: - **Wrapping the drawer creates a new stacking context**, so its z-order is restated on the wrapper, and `pointerEvents='box-none'` keeps the full-screen wrapper from swallowing touches meant for the screen behind it. This is the same class of thing the tab bar work already had to handle. - **Pinned to `0` while the drawer is open** — that same drawer is the full-screen player, and translating it there would drag the expanded player off screen. ## Verification (iOS simulator, track actually playing) - Chrome hides and restores as one unit, no empty band. - Full-screen player still opens correctly through the wrapper. - Swipe-to-dismiss still works. - Left nav drawer still pushes the play bar with the screen rather than being punctured by its elevation — the specific z-order risk of wrapping. - No crashes across the run. `tsc` 0 errors, `eslint` clean. ## Not verified **Android.** This is a shared, gesture-heavy component and Android resolves z-order via `elevation` rather than `zIndex`, which is exactly where the wrapper could behave differently. Worth an Android check before or shortly after merge. ## Unrelated, but noticed Two `SIGABRT`s occurred on the simulator during this session, both `ShadowTree::commit` asserts inside `reanimated::ReanimatedMountHook::shadowTreeDidMount`. Both followed Fast Refresh reload cycles; clean launches have been stable through playback, scrolling, drawer open/close and tab switching. Likely a dev-only artifact — that assert is typically compiled out in release — but noting it since it first appeared during the #14575 reanimated work rather than before it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Gives the root tab screens a frosted header that content scrolls behind — matching the desktop client's
Frostedsurface — and lets the top and bottom chrome get out of the way as you scroll.In scope: Feed, Trending, Notifications.
Deliberately not: Explore (keeps its own search header) and Library (its material top-tab bar is opaque and shared with Profile, so the glass had a visible seam — left exactly as it was).
How it fits together
GlassSurface— the shared frosted surface. RealBlurViewon iOS; on Android a near-opaque tint instead, matching the existingProfileNavOverlayprecedent, since Android blur on a surface composited every scroll frame is a real perf risk.GlassChromeContext— owns a singlescrollYshared value. The separator fade, the header translate and the tab bar translate all read from it, so they can't drift out of sync.headerTransparent, so screens pad their own scroll content by the measured stack height. Measured rather than computed because the safe-area inset varies per device andOtaUpdateBanneradds a row conditionally.PlayBarChin→BottomChinThe rename carries a behaviour change worth reviewing. It previously reserved only
PLAY_BAR_HEIGHT; with the tab bar out of flow it now covers both bars. Without that, the end of every list would sit behind the tab bar permanently — worst on short lists, which never scroll far enough for the auto-hide to uncover them. This is about the end of the content being reachable, not about avoiding overlap mid-scroll: content still passes under the glass while scrolling, which is the point.It also touches three Library files and
FanClubsExploreScreen— import lines only, no behaviour change there.Testing
tsc --noEmitclean,eslintclean.blurTypebranch are both unexercised paths. Dark-mode pass is in progress.No changeset, on purpose
No SDK surface, and a
'@audius/mobile'bump moves the OTA release-history routing key — that's what broke production OTA in August (see #14564).production/1.5.185(what iPhones poll) hasn't had a release since 2026-07-28. Merging this publishes torconly; it needs #14564 merged and a manual production dispatch to reach users.🤖 Generated with Claude Code